package mpicbg.models;

import java.util.ArrayList;
import java.util.Random;
import java.util.List;
import java.util.Collection;

public abstract class Model {
	
	abstract public int getMinSetSize();
	
	final static protected Random rnd = new Random( 69997 );


	protected double error = Double.MAX_VALUE;
	final public double getError(){ return error; }
	final public void setError( double e ){ error = e; }

	public boolean betterThan( Model m )
	{
		if ( error < 0 ) return false;
		return error < m.error;
	}

		
	abstract public float[] apply( float[] point );

	abstract public void applyInPlace( float[] point );

	abstract public float[] applyInverse( float[] point ) throws NoninvertibleModelException;

	abstract public void applyInverseInPlace( float[] point ) throws NoninvertibleModelException;
	abstract public void shake(
			Collection< PointMatch > matches,
			float scale,
			float[] center );
		
	abstract public void fit( Collection< PointMatch > matches ) throws NotEnoughDataPointsException;

	final static public boolean test(
			Model model,
			Collection< PointMatch > candidates,
			Collection< PointMatch > inliers,
			double epsilon,
			double min_inlier_ratio )
	{
		inliers.clear();
		
		for ( PointMatch m : candidates )
		{
			m.apply( model );
			if ( m.getDistance() < epsilon ) inliers.add( m );
		}
		
		float ir = ( float )inliers.size() / ( float )candidates.size();
		model.error = Math.max( 0.0, Math.min( 1.0, 1.0 - ir ) );
		
		return ( ir > min_inlier_ratio );
	}

	final static public < M extends Model >M filter(
			Class< M > modelType,
			Collection< PointMatch > candidates,
			Collection< PointMatch > inliers ) throws NotEnoughDataPointsException
	{
		M model;
		try
		{
			model = modelType.newInstance();
		}
		catch ( Exception e )
		{
			System.err.println( e.getMessage() );
			return null;
		}
		
		inliers.clear();
		inliers.addAll( candidates );
		final ArrayList< PointMatch > temp = new ArrayList< PointMatch >();
		int num_inliers;
		do
		{
			temp.clear();
			temp.addAll( inliers );
			num_inliers = inliers.size(); 
			model.fit( inliers );
			ErrorStatistic observer = new ErrorStatistic();
			for ( PointMatch m : temp )
			{
				m.apply( model );
				observer.add( m.getDistance() );
			}
			inliers.clear();
			double t = observer.median * 4;
			for ( PointMatch m : temp )
			{
				if ( m.getDistance() < t )
					inliers.add( m );
			}
			//System.out.println( ( num_inliers - inliers.size() ) + " candidates with e > " + t + " removed by iterative robust regression." );
			//System.out.println( inliers.size() + " inliers remaining." );
			
			model.error = observer.mean;
		}
		while ( num_inliers > inliers.size() );
		
		if ( num_inliers < model.getMinSetSize() )
			return null;
		return model;
	}

	final static public < M extends Model >M ransac(
			Class< M > modelType,
			List< PointMatch > candidates,
			Collection< PointMatch > inliers,
			int iterations,
			double epsilon,
			double min_inlier_ratio ) throws NotEnoughDataPointsException
	{
		M model;
		try
		{
			model = modelType.newInstance();
		}
		catch ( Exception e )
		{
			System.err.println( e.getMessage() );
			return null;
		}
		
		final int MIN_SET_SIZE = model.getMinSetSize();
		
		inliers.clear();
		
		if ( candidates.size() < MIN_SET_SIZE )
		{
			throw new NotEnoughDataPointsException( candidates.size() + " correspondences are not enough to estimate a model, at least " + MIN_SET_SIZE + " correspondences required." );
		}
		
		int i = 0;
		final ArrayList< PointMatch > min_matches = new ArrayList< PointMatch >();
		
		while ( i < iterations )
		{
			// choose model.MIN_SET_SIZE disjunctive matches randomly
			min_matches.clear();
			for ( int j = 0; j < MIN_SET_SIZE; ++j )
			{
				PointMatch p;
				do
				{
					p = candidates.get( ( int )( rnd.nextDouble() * candidates.size() ) );
				}
				while ( min_matches.contains( p ) );
				min_matches.add( p );
			}
			M m;
			try
			{
				m = modelType.newInstance();
			}
			catch ( Exception e )
			{
				System.err.println( e.getMessage() );
				return null;
			}
			final ArrayList< PointMatch > temp_inliers = new ArrayList< PointMatch >();
			m.fit( min_matches );
			int num_inliers = 0;
			boolean is_good = test( m, candidates, temp_inliers, epsilon, min_inlier_ratio );
			while ( is_good && num_inliers < temp_inliers.size() )
			{
				num_inliers = temp_inliers.size();
				m.fit( temp_inliers );
				is_good = test( m, candidates, temp_inliers, epsilon, min_inlier_ratio );
			}
			if (
					is_good &&
					m.betterThan( model ) &&
					temp_inliers.size() >= 3 * MIN_SET_SIZE )
			{
				model = m;
				inliers.clear();
				inliers.addAll( temp_inliers );
			}
			++i;
		}
		if ( inliers.size() == 0 )
			return null;
		return model;
	}
	
	static public  < M extends Model >M filterRansac(
			Class< M > modelType,
			List< PointMatch > candidates,
			Collection< PointMatch > inliers,
			int iterations,
			float max_epsilon,
			float min_inlier_ratio ) throws NotEnoughDataPointsException
	{
		final ArrayList< PointMatch > temp = new ArrayList< PointMatch >();
		ransac(
				modelType,
				candidates,
				temp,
				iterations,
				max_epsilon,
				min_inlier_ratio );		
		return filter( modelType, temp, inliers );
	}
	
	abstract public String toString();

	
	abstract public Model clone();
};
